/*
 *   The contents of this file are subject to the Mozilla Public License
 *   Version 1.1 (the "License"); you may not use this file except in
 *   compliance with the License. You may obtain a copy of the License at
 *   http://www.mozilla.org/MPL/
 *
 *   Software distributed under the License is distributed on an "AS IS"
 *   basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 *   License for the specific language governing rights and limitations
 *   under the License.
 *
 *   The Original Code is Matra - the DTD Parser.
 *
 *   The Initial Developer of the Original Code is Conrad S Roche.
 *   Portions created by Conrad S Roche are Copyright (C) Conrad 
 *   S Roche. All Rights Reserved.
 *
 *   Alternatively, the contents of this file may be used under the terms
 *   of the GNU GENERAL PUBLIC LICENSE Version 2 or any later version
 *   (the  "[GPL] License"), in which case the
 *   provisions of GPL License are applicable instead of those
 *   above.  If you wish to allow use of your version of this file only
 *   under the terms of the GPL License and not to allow others to use
 *   your version of this file under the MPL, indicate your decision by
 *   deleting  the provisions above and replace  them with the notice and
 *   other provisions required by the GPL License.  If you do not delete
 *   the provisions above, a recipient may use your version of this file
 *   under either the MPL or the GPL License."
 *
 *   [NOTE: The text of this Exhibit A may differ slightly from the text of
 *   the notices in the Source Code files of the Original Code. You should
 *   use the text of this Exhibit A rather than the text found in the
 *   Original Code Source Code for Your Modifications.]
 *
 * Created: Conrad S Roche <derupe at users.sourceforge.net>,  25-Sep-2003
 */
package com.conradroche.matra.parser;

import java.util.ArrayList;

import com.conradroche.dtd.decl.AttDef;
import com.conradroche.dtd.decl.AttlistDecl;
import com.conradroche.dtd.parser.AttlistReader;
import com.conradroche.matra.data.DTDData;
import com.conradroche.matra.data.Data;
import com.conradroche.matra.exception.DTDSyntaxException;

/**
 * Class for reading an attribute
 * list declaration.
 *
 * @author Conrad Roche
 */
public class AttlistReaderImpl implements AttlistReader {

	/**
	 * Token identifying an attribute list declaration.
	 */
	private static final String ATTLIST_START = "<!ATTLIST";
	private static final String OPT_REQUIRED  = "#REQUIRED";
	private static final String OPT_IMPLIED   = "#IMPLIED";
	private static final String OPT_FIXED     = "#FIXED";
	
	private static final String NOTATION = "NOTATION";
	
	/**
	 * AttlistReaderImpl Constructor.
	 */
	public AttlistReaderImpl() {
		super();
	}

	/**
	 * Read the Attribute list declaration 
	 * from the data stream.
	 * 
	 * @param data The stream from which to read the
	 * 			Attribute list declaration.
	 * 
	 * @return The Attribute list.
	 * 
	 * @throws DTDSyntaxException If the attribute 
	 * 			list declaration contains a syntax
	 * 			error. 
	 * 
	 * @see com.conradroche.dtd.parser.AttlistReader#readAttlist(com.conradroche.matra.data.DTDData)
	 */
	public AttlistDecl readAttlist(DTDData data) throws DTDSyntaxException {
		
		if(!isAttlistStart(data)) {
			return null; 
		}
		
//		[52] AttlistDecl ::= '<!ATTLIST' S Name AttDef* S? '>'

		//CR: TODO: Instantiate an implementation of AttlistDecl !!! 
		AttlistDecl attlist = null;

		data.getNextToken(); //'<!ATTLIST'
		data.skipWhiteSpace(); // S
		
		//CR: TODO: better to get next token and check if its a name.
		String elemName = data.getNextName(); // Name
		attlist.setElementName(elemName);
		
		readAttributeDefs(attlist, data);
		
		data.skipWhiteSpace(); // S?
		
		if(data.lookNextChar() != '>') {
			throw new DTDSyntaxException("Invalid AttList block encountered at location " + 
						data.getCurrentLocation() + ". Was expecting '>', got char '" + data.lookNextChar() + "'."); 
		}

		data.getNextChar(); // '>'
		
		return attlist;
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readAttributeDefs
	 * 
	 * @param attlist
	 * @param data
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readAttributeDefs(AttlistDecl attlist, DTDData data) throws DTDSyntaxException {

		AttDef attdef;
		
		while(data.lookNextChar() != '>') {
			attdef = readAttDef(data);
			attlist.addAttDef(attdef);
			data.skipWhiteSpace();
		} 
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readAttDef
	 * 
	 * @param data
	 * 
	 * @return
	 * 
	 * @throws DTDSyntaxException 
	 */
	private AttDef readAttDef(DTDData data) throws DTDSyntaxException {

//		[53] AttDef ::= S Name S AttType S DefaultDecl

		//use an implementation of AttDef!
		AttDef attdef = null;
		
		data.skipWhiteSpace(); // S
		
		//CR: TODO: better to get next token and check if its a name.
		String attrName = data.getNextName();
		attdef.setAttributeName(attrName);
		
		data.skipWhiteSpace(); // S
		
		readAttType(attdef, data); // AttType
		
		data.skipWhiteSpace(); // S
		
		readDefaultDecl(attdef, data); // DefaultDecl
		
		return attdef;
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readAttType
	 * 
	 * @param attdef
	 * @param data
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readAttType(AttDef attdef, DTDData data) throws DTDSyntaxException {

		//[54]    AttType    ::=    StringType | TokenizedType | EnumeratedType  
		//[55]    StringType    ::=    'CDATA' 
		//[56]    TokenizedType    ::=    'ID' | 'IDREF' | 'IDREFS' | 'ENTITY' | 'ENTITIES' | 'NMTOKEN' | 'NMTOKENS'
		//[57]    EnumeratedType    ::=    NotationType | Enumeration 
		//[58]    NotationType    ::=    'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')' 
		//[59]    Enumeration    ::=    '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')' 
		
		//Enumeration
		if(data.lookNextChar() == '(') {
			readEnumeration(attdef, data);
			return;
		}
		
		//NotationType
		if(data.nextTokenEquals(NOTATION)) {
			data.getNextToken(); // 'NOTATION'
			data.skipWhiteSpace(); // S
			readNotationType(attdef, data);
			return;
		}

		//StringType | TokenizedType
		String token = data.getNextToken();
		setAttributeType(attdef, token);
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::setAttributeType
	 * 
	 * @param attdef
	 * @param token
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void setAttributeType(AttDef attdef, String token) throws DTDSyntaxException {

		//[55]    StringType    ::=    'CDATA' 
		//[56]    TokenizedType    ::=    'ID' | 'IDREF' | 'IDREFS' | 'ENTITY' | 'ENTITIES' | 'NMTOKEN' | 'NMTOKENS'
		
		if(token.equals("CDATA")) {
			attdef.setAttType(AttDef.TYPE_STRING);
		} else if(token.equals("ID")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_ID);
		} else if(token.equals("IDREF")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_IDREF);
		} else if(token.equals("IDREFS")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_IDREFS);
		} else if(token.equals("ENTITY")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_ENTITY);
		} else if(token.equals("ENTITIES")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_ENTITIES);
		} else if(token.equals("NMTOKEN")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_NMTOKEN);
		} else if(token.equals("NMTOKENS")) {
			attdef.setAttType(AttDef.TYPE_TOKENIZED_NMTOKENS);
		} else {
			throw new DTDSyntaxException("Invalid attribute type specified in the attribute definition - '"
							+ token + "'.");
		}
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readNotationType
	 * 
	 * @param attdef
	 * @param data
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readNotationType(AttDef attdef, DTDData data) throws DTDSyntaxException {
		
		attdef.setAttType(AttDef.TYPE_ENUM_NOTATION);
		readEnumeratedType(attdef, data, true);
		
	}

	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readEnumeration
	 * 
	 * @param attdef
	 * @param data
	 * @throws DTDSyntaxException
	 */
	private void readEnumeration(AttDef attdef, DTDData data) throws DTDSyntaxException {
		
		attdef.setAttType(AttDef.TYPE_ENUM_ENUMERATION);
		readEnumeratedType(attdef, data, false);
	}
		
	/**
	 * CR: JavaDoc: Add javadoc for AttlistReaderImpl::readEnumeration
	 * 
	 * @param attdef
	 * @param data
	 * @param isNotation  
	 * 
 	 * @throws DTDSyntaxException 
	 */
	private void readEnumeratedType(AttDef attdef, DTDData data, boolean isNotation) throws DTDSyntaxException {
		
		ArrayList enumeration = new ArrayList();
	
		//[58]    NotationType    ::=    'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')' 
		//[59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'

		if(data.lookNextChar() != '(') {
			throw new DTDSyntaxException("Enumerations must begin with '('. Found invalid char - '" + 
							data.lookNextChar() + "' while parsing attribute " + attdef.getAttributeName()); 
		}
		
		data.getNextChar(); // '('
		data.skipWhiteSpace(); // S?

		String token = (isNotation ? data.getNextName() : data.getNextNmToken()); // Name : Nmtoken
	
		if(token == null) {
			throw new DTDSyntaxException("Did not find any valid enumerations following the '(' in the attribute definition for " +
						attdef.getAttributeName() + ". Found invalid char - " + data.lookNextChar());
			//there should be at least one value in an enumeration
		}

		while(token != null) {
			enumeration.add(token);
			data.skipWhiteSpace(); // S?
		
			if(data.checkNextChar(')') == Data.FOUND_CHAR) { // ')'
				attdef.setEnumeratedValues(enumeration);
				return;
			} else if(data.checkNextChar('|') == Data.FOUND_CHAR) { // '|'
				data.skipWhiteSpace(); // S?
				token = (isNotation ? data.getNextName() : data.getNextNmToken()); // Name : Nmtoken
			} else {
				throw new DTDSyntaxException("Found invalid char - '" + data.lookNextChar() + "' while parsing for enumerations.");
			}
		}
	}

	/**
	 * Read the default declaration in the attribute definition.
	 * 
	 * @param attdef The AttDef object in which the attribute definition
	 * 			is to be stored.
	 * @param data The data stream from which the attribute definition
	 * 			is being read.
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readDefaultDecl(AttDef attdef, DTDData data) throws DTDSyntaxException {
		
		//[60] DefaultDecl ::=  '#REQUIRED' | '#IMPLIED' | (('#FIXED' S)? AttValue)

		//The first char will either be a '#' or a quote (either ' or ")
		
		if(data.lookNextChar() == '#') {
			String optionality = data.getNextToken();
			if(optionality.equals(OPT_REQUIRED)) {
				attdef.setDefaultDeclType(AttDef.OPTIONALITY_REQUIRED);
			} else if(optionality.equals(OPT_IMPLIED)) {
				attdef.setDefaultDeclType(AttDef.OPTIONALITY_IMPLIED);
			} else if(optionality.equals(OPT_FIXED)) {
				 
				attdef.setDefaultDeclType(AttDef.OPTIONALITY_FIXED);
				
				data.skipWhiteSpace(); // S
				
				String attValue = readAttValue(data); // AttValue
				attdef.setDefaultValue(attValue);
			}
		} else {
			String attValue = readAttValue(data); // AttValue
			attdef.setDefaultValue(attValue);
		}
	}

	/**
	 * Read the default value specified in the attribute definition.
	 * 
	 * @param data The data stream to read from.
	 * 
	 * @return The default attribute value.
	 * 
	 * @throws DTDSyntaxException 
	 */
	private String readAttValue(DTDData data) throws DTDSyntaxException {
		//[10] AttValue ::= '"' ([^<&"] | Reference)* '"'  |  "'" ([^<&'] | Reference)* "'"
		
		char quot = data.lookNextChar();
		
		if(quot != '\'' && quot != '\"') {
			throw new DTDSyntaxException(
				"The default value for the attribute, in the attribute definition, should be enclosed in quotes (\' or \"." +
				"Encountered char '" + quot + "' instead.");
		}
		
		//CR: TODO: This method should throw an exception if EOF encountered before delim.
		String defaultValue = data.getNextToken(quot);
		
		//CR: TODO: Do more checks on the attribute default value - Maybe not in this class.
		// i) There should be no '<' char in this value.
		// ii) If an & is encountered, it should follow the Reference production.
		
		//[67]    Reference    ::=    EntityRef | CharRef 
		//[68]    EntityRef    ::=    '&' Name ';'  
		//[69]    PEReference    ::=    '%' Name '; 

		return defaultValue;
	}

	/**
	 * Checks if there is an attribute list
	 * declaration at the current location 
	 * of the data.
	 * 
	 * @param data The data to be parsed.
	 * 
	 * @return <code>true</code> if the current location
	 * 	of the data has an attrubute list declaration; 
	 * 	<code>false</code> otherwise.
	 * 
	 * @see com.conradroche.dtd.parser.AttlistReader#isAttlistStart(com.conradroche.matra.data.DTDData)
	 */
	public boolean isAttlistStart(DTDData data) {

		if(data == null || data.endOfData()) {
			return false;		
		}
		
		return data.nextTokenEquals(ATTLIST_START);
	}
}
